在前幾篇中,我們嘗試自己獲取全局error、promise error、以及前端框架級別error(react error-boundary)。但就如同之前提到的---如果工程師只獲取error以及其console,不知道用戶是如何操作因而造成這個error的。根據我們之前的 demo,知道可以在Sentry平台中的Breadcrumbs中查看到user 在觸發 error前的一系列動作。
Sentry中的Breadcrumbs,是用來記錄用戶一系列交互的機制,如點擊、輸入、頁面加載等等,這些操作會記錄在以一個類似log的array中,在程式發生異常的時候一起發送給Sentry。
在client side,Sentry SDK只會存100條breadcrumb 記錄(packages/core/src/scope.ts
中有個const DEFAULT_MAX_BREADCRUMBS = 100;
常量,限制了最多為100條)
查看Sentry js SDK中的packages/browser-utils/src/instrument/dom.ts
,可以知道Sentry會攔截user-event中的click和keypress,並且賦予event_id、同時debounce,這樣一來可以透過檢查event_id的有無重複、以及在一定時間內只從同一個元素獲取event,來有效率的獲取user-action
至於要如何攔截?從 document.addEventListener('click', callback, true);
入手,讓攔截器在事件捕獲階段就被監聽到。
在browser的事件監聽機制中,分為三大階段:
from《What is Event Bubbling and Capturing and how to handle them?
》
而捕獲階段,事件會從全局(window)往下找尋找event相應的target。因此所有的事件都會從全局的捕獲階段開始,而如果是監聽冒泡階段的話,可能會被目標節點的父節點或者祖先節點給攔截(event.stopPropagation()
),這樣一來就可能會監聽不到。這是為何要在全局事件捕獲階段掛載監聽器的原因。
根據在 Sentry Breadcrumbs上的展示:
可以看到我們需要獲取該dom的節點路徑,如 body.vsc-initialized > div#root > div > div > button
這個邏輯,可以先獲取該event的target,然後在從其parentElement
屬性,一層一層往上記錄:
cosnt getElementPath = (element) => {
if (!element) return '';
let path = [];
while (element) {
let name = element.nodeName.toLowerCase();
if (element.id) {
name += `#${element.id}`;
} else if (element.className) {
name += `.${element.className.split(' ').join('.')}`;
}
path.unshift(name);
element = element.parentElement;
}
return path.join(' > ');
};
先撇除debounce和事件id比較,直接利用上述的事件捕獲器和解析dom階段,寫一個
基於 click 事件的user-actions記錄:
self-instrument-dom.js
export class SelfDomInstrument {
constructor() {
this.userActions = [];
}
captureEvent(event) {
if (event.type != 'click') return;
const target = event.target;
// TODO:檢查是否和上次catch的事件相似
if (true) {
const path = this._getElementPath(target);
this.userActions.push({
type: 'UI Click',
time: new Date().toISOString(),
path,
});
}
}
_getElementPath = (element) => {
if (!element) return '';
let path = [];
while (element) {
let name = element.nodeName.toLowerCase();
if (element.id) {
name += `#${element.id}`;
} else if (element.className) {
name += `.${element.className.split(' ').join('.')}`;
}
path.unshift(name);
element = element.parentElement;
}
return path.join(' > ');
};
initEventMonintoring() {
document.addEventListener('click', this.captureEvent.bind(this), true);
}
}
同樣,根據之前的全局error攔截器,我們可以加上window.onerror = callback
的邏輯,在error發生的時候,打印出之前記錄的 user-actions:
initEventMonintoring() {
document.addEventListener('click', this.captureEvent.bind(this), true);
window.onerror = this._errorHandler.bind(this);
}
_errorHandler(message, source, lineno, colno, error) {
console.log('---user actions with error:', [
...this.userActions,
{
type: 'Error',
time: new Date().toISOString(),
message,
},
]);
}
可以拿出之前的 InOrderError.jsx
組件,順序點擊button,便可以在瀏覽器console中看到我們所記錄的user-actions 以及捕獲到的 error:
我們知道了解了如何透過browser的事件捕獲階段、獲取全局的用戶事件,然後demo了監聽全局 click 事件,並且記錄下所有的user有使用click的target dom,在 error 發生的時候能夠打印所有的 user click actions,進而了解了Sentry的 Breadcrumbs機制。
本文的程式碼可以在Github repository上查看。